[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写

字段路径的完整语法、读写语义和限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 Go SDK 接口与接入示例。

1. 接口说明

TDR 表的 Blob 字段(char/Byte 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。

三个接口对应三个命令字:

接口 命令字 说明
DoPBFieldGet TcaplusApiPBFieldGetReq 0x0067 读取 Blob 中指定的 PB 字段
DoPBFieldUpdate TcaplusApiPBFieldUpdateReq 0x0069 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入
DoPBBatchFieldGet TcaplusApiPBBatchFieldGetReq 0x0075 多个主键共用一组字段路径批量读取

Blob 中 Protobuf 字段的自增(TcaplusApiPBFieldIncreaseReq)在 TDR 表上不支持。

2. 版本要求

  • Go SDK 的 v0.6.36 已包含本功能;使用其他版本时,请确认提供本文所述接口。
  • 服务端需要支持该特性。未适配的服务端在这三个命令上会直接返回找不到 PB 描述的错误,使用前请先确认服务端版本。

3. 准备工作

参见准备工作文档,完成使用该接口前的准备工作,并创建TDR Generic表。 service_info表service_info.xml

本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段,service_info 表中对应的是:

<entry name="routeinfo_len"     type="uint"   defaultvalue="0" desc="路由规则信息长度" />
<entry name="routeinfo"         type="char"   count="1024" refer="routeinfo_len" desc="路由规则信息" />

routeinfo 是保存 PB 的 Blob 字段,routeinfo_len 是它的 refer 长度字段, 这两个字段在下面的示例里分别对应 Go 结构体的 RouteinfoRouteinfo_Len

Blob 里的 PB 定义示例(RouteInfo,不是 Tcaplus PB 表,只是普通 proto,不需要 tcaplus 的 proto 选项):

syntax = "proto3";
package routeinfo;

message RouteInfo {
  uint32 version = 1;
  string strategy = 2;
  Weight weight = 3;
  repeated string tags = 4;
  repeated Instance instances = 5;
  map<int64, Instance> instance_map = 6;
  map<string, string> settings = 7;
}
message Weight { uint32 cpu = 1; uint32 memory = 2; }
message Instance { string addr = 1; uint32 port = 2; uint32 weight = 3; }

该 PB 定义与生成代码见示例目录

4. 字段路径

4.1 按 PB 字段名构造路径

tdrpb.BuildPaths 描述要操作的字段。Blob 接收 TDR Blob 前缀、PB 类型和相对 PB 根 message 的字段名路径,SDK 完成 name path 到 tagid path 的转换,无需业务自行封装。

paths, err := tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy", "weight.cpu"),
    tdrpb.Raw("filterdata"), // 原生 TDR 一级字段
)
if err != nil {
    return
}
opt := &option.TDROpt{FieldNames: paths}

Blob 中的 PB 参数只用于获取类型,不提供更新值。更新数据由增量 PB 提供,见下一节。删除 map 元素时使用 tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),生成 POP routeinfo.6[1001]不通过 opt.Operation 传递

FieldNames 中无需手动加入 routeinforouteinfo_len:SDK 自动补齐一级字段及所需 refer。一次请求也可以通过多个 Blob 分组选择不同 Blob 中的字段。

4.2 BuildPaths 与 SetFieldNames

SetFieldNames 原来用于选择 TDR 一级字段,本功能将其扩展到 Blob 内嵌的 PB 字段,因此可以在同一列表中选择 PB 字段和原生 TDR 一级 value 字段。嵌套 PB 路径须使用本文的 DoPB* 接口,普通 TDR Get/Update 的字段选择规则不变。

BuildPaths 为这一步提供按字段名构造路径的封装:先将 PB 字段名转换为字段编号,再将结果赋给 option.TDROpt.FieldNames,由 DoPB* 接口构造请求并调用 SetFieldNames。对应路径如下:

PB 定义 name path 完整 tagid path
uint32 version = 1 version routeinfo.1
Weight weight = 3 weight.cpu routeinfo.3.1
repeated Instance instances = 5 instances[0] routeinfo.5#[0]
map<int64, Instance> instance_map = 6 instance_map[1001] routeinfo.6[1001]
同上 instance_map[1001].addr routeinfo.6[1001].1
map<string, string> settings = 7 settings['region'] routeinfo.7['region']

需要直接指定字段编号时,可以填写 []string{"routeinfo.1", "routeinfo.2", "filterdata"},或通过 tdrpb.Raw 原样传入完整 tagid path。SetFieldNames 不接收 routeinfo.version 这样的 PB 字段名路径。

服务端没有业务 proto,tagid path 中用 [key] 区分 map、#[index] 区分 repeated;字段名接口根据 PB 类型完成这一转换。一条 PB 路径最多访问一层容器元素;packed 数组只能整组读写,不支持业务元素下标。路径冲突、类型限制及 repeated 范围读取的完整规则见《TDR Blob 内嵌 Protobuf 部分字段读写》。

5. 示例代码

示例代码见示例目录, 本特性相关的示例文件为 pbfieldprepare.gopbfieldget.gopbfieldupdate.gopbbatchfieldget.go, 入口在 main.go 中。

Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了。先整段写一份完整 PB(对应示例文件的 pbFieldPrepareExample):

full := &routeinfo.RouteInfo{Version: 1, Strategy: "round_robin", /* ... */}

// 增量/整段 PB 都用 partial 语义序列化,proto2 缺 required 字段时也能正常处理
buf, err := tdrpb.MarshalPartial(full)
if err != nil {
    return
}

data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
data.Routeinfo = buf
data.Routeinfo_Len = uint32(len(buf))

// 用 Replace 而不是 Insert,示例可以反复跑
if err = client.DoReplace(TableName, data, nil); err != nil {
    return
}

5.1 读取部分字段

对应示例文件的 pbFieldGetExamplepbFieldGetContainerExample

data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"

opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy", "weight.cpu"),
    tdrpb.Raw("filterdata"),
); err != nil {
    return
}

if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
    return
}

// Routeinfo_Len 是本次部分 PB 的长度,不是记录里完整 Blob 的长度
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(data.Routeinfo, data.Routeinfo_Len, partial); err != nil {
    return
}
fmt.Println(partial.GetVersion(), partial.GetStrategy(), partial.GetWeight().GetCpu(), data.Filterdata)

只有请求过的字段有值,未请求的字段是未设置状态,返回的部分记录不能当完整记录用

5.2 更新部分字段

对应示例文件的 pbFieldUpdateExample(部分更新)、pbFieldUpdateMapExample(map 元素覆盖与删除)。

delta := &routeinfo.RouteInfo{Version: 3, Strategy: "weighted_round_robin"}

data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
data.Routeinfo = make([]byte, 1024) // Init 不会给 Blob 分配空间,tdr_count 是 1024
n, err := tdrpb.MarshalPartialTo(data.Routeinfo, delta)
if err != nil {
    return
}
data.Routeinfo_Len = uint32(n)

opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
); err != nil {
    return
}

// 增量只带本次要写的字段,Blob 里其余字段服务端原样保留
if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
    return
}

TDR 的 tinyint 数组会被生成为 []int8,这种表换用 tdrpb.MarshalPartialToInt8 / tdrpb.UnmarshalPartialInt8,参数含义一致。

5.3 批量读取

对应示例文件的 pbBatchFieldGetExample

var dataSlice []record.TdrTableSt
for i := 0; i < 10; i++ {
    data := service_info.NewService_Info()
    data.Gameid = "dev"
    data.Envdata = "oa"
    data.Name = fmt.Sprintf("%d", i)
    dataSlice = append(dataSlice, data)
}

// 字段路径对所有主键生效,单条记录的结果看 opt.BatchResult,版本看 opt.BatchVersion
opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
    tdrpb.Raw("filterdata"),
); err != nil {
    return
}

// 请求级成功不等于每条记录都成功,需要逐条检查 opt.BatchResult
if err = client.DoPBBatchFieldGet(TableName, dataSlice, opt); err != nil {
    fmt.Printf("batch field get: %s\n", err)
    // 可能只有部分记录失败,继续检查已经返回的逐条结果。
}
if len(opt.BatchResult) != len(dataSlice) {
    return // 请求构造失败等情况下,逐条结果尚未建立。
}

for i, data := range dataSlice {
    if opt.BatchResult[i] != nil {
        fmt.Printf("record %d failed: %s\n", i, opt.BatchResult[i].Error())
        continue
    }
    row := data.(*service_info.Service_Info)
    partial := &routeinfo.RouteInfo{}
    if err = tdrpb.UnmarshalPartial(row.Routeinfo, row.Routeinfo_Len, partial); err != nil {
        continue
    }
    fmt.Printf("record %d version %d, filterdata %s\n", i, opt.BatchVersion[i], row.Filterdata)
}

单次最多 1024 条主键,响应不保证顺序,按主键回填到 dataSlice 的对应下标。 分包由服务端通过响应里的 LeftNum 驱动,SDK 已经收完所有分包,不需要设置 MultiFlag

6. option 支持情况

opt 必填且 opt.FieldNames 不能为空。不支持的选项会返回错误;批量 Condition 返回 GEN_ERR_INVALID_ARGUMENTS

option FieldGet FieldUpdate BatchFieldGet
FieldNames 必填 必填 必填
Condition 支持 支持 不支持(请求体没有条件字段)
Timeout / Ctx / Flags / UserBuff 支持 支持 支持
Version 支持,入参兼出参 支持 拒绝,版本只从 BatchVersion 出参
VersionPolicy 拒绝 支持 拒绝
ResultFlagForSuccess / ResultFlagForFail 拒绝 支持 拒绝
BatchResult / BatchVersion 不适用 不适用 支持
MultiFlag / Operation / ResultFlag / IncField / TTL / Limit / Offset / ExpireTime 拒绝 拒绝 拒绝

VersionDoPBFieldGet 上既是入参也是出参,响应回来时被改写为记录的当前版本。

7. 错误码

本地检查(接口返回值):

场景 错误码
opt 为 nil、FieldNames 为空、使用了不支持的 option ParameterInvalid
路径为空或超过 1023 字节 API_ERR_OVER_MAX_FIELD_NAME_LEN
补齐后字段数超过 256 API_ERR_OVER_MAX_VALUE_FIELD_NUM
路径重复或父子路径冲突、未先 SetData API_ERR_PARAMETER_INVALID
路径的一级字段不是本表的 value 字段 API_ERR_FIELD_NOT_EXSIST
PB 字段不存在 API_ERR_FIELD_NOT_EXSIST
map / repeated / 嵌套 message 类型不匹配 API_ERR_FIELD_TYPE_NOT_MATCH
Blob 容量不足 API_ERR_OVER_MAX_FIELD_VALUE_LEN

发送后的结果看接口返回值;批量还要看每条 opt.BatchResult。常见服务端错误码:

错误码 含义
TXHDB_ERR_RECORD_NOT_EXIST 记录不存在
COMMON_ERR_ELEMENT_NOT_EXIST 指定的 map key 或 repeated 下标不存在
COMMON_ERR_CONDITION_NOT_MATCHED 条件不成立
SVR_ERR_FAIL_INVALID_VERSION 版本校验失败

详见错误码含义和处理方法

8. 注意事项

  1. 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
  2. Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
  3. 返回的 routeinfo_len本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。
  4. SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责, tdrpb 只提供 MarshalPartial / MarshalPartialTo / UnmarshalPartial 等便利函数。
  5. 路径里的一级字段名取 TDR 表定义中的字段名(tdr_field),不是 Go 结构体字段名。
  6. 字段名使用 .proto 中的原始 field name,不使用 JSON name。
  7. 嵌套结构体里的 Blob(如 extra.bin)也支持,SDK 自动补齐其所属的一级结构体字段。

9. 其它参考文档

TDR Blob 内嵌 Protobuf 部分字段读写

[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写

[TDR Generic表][Go SDK]查询单条数据

[TDR Generic表][Go SDK]条件过滤和更新说明

results matching ""

    No results matching ""